//
// Created by gastonfeng on 2022/11/21.
//

#if defined(USE_USB_SERIAL)

#include "USB_Serial.h"
#include "cdc_queue.h"
#include "core_debug.h"
#include "ulp.h"
#include "usbd_cdc.h"
#include "usbd_desc.h"
#include "wiring.h"
#include <Rtos.h>
#include <stm32yyxx_ll_usb.h>
#include <usbd_def.h>


#if defined(USE_USB_HS)
#define USBD_USB_INSTANCE USB_OTG_HS
#define USBD_DP_PINNAME USB_OTG_HS_DP
#elif defined(USB_OTG_FS)
#define USBD_USB_INSTANCE USB_OTG_FS
#define USBD_DP_PINNAME USB_OTG_FS_DP
#elif defined(USB)
#define USBD_USB_INSTANCE USB
#define USBD_DP_PINNAME USB_DP
#endif
extern __IO bool dtrState_Queue;
extern __IO bool rtsState_Queue;

/**
 ******************************************************************************
 * @file    usbd_cdc_if.c
 * @author  MCD Application Team
 * @brief   Generic media access Layer.
 ******************************************************************************
 * @attention
 *
 * <h2><center>&copy; Copyright (c) 2015 STMicroelectronics.
 * All rights reserved.</center></h2>
 *
 * This software component is licensed by ST under Ultimate Liberty license
 * SLA0044, the "License"; You may not use this file except in compliance with
 * the License. You may obtain a copy of the License at:
 *                      www.st.com/SLA0044
 *
 ******************************************************************************
 */

/* Includes ------------------------------------------------------------------*/

#ifdef USE_USB_HS
#define CDC_MAX_PACKET_SIZE USB_OTG_HS_MAX_PACKET_SIZE
#elif defined(USB_OTG_FS) || defined(USB_OTG_FS_MAX_PACKET_SIZE)
#define CDC_MAX_PACKET_SIZE USB_OTG_FS_MAX_PACKET_SIZE
#else /* USB */
#define CDC_MAX_PACKET_SIZE USB_MAX_EP0_SIZE
#endif

/*
 * The value USB_CDC_TRANSMIT_TIMEOUT is defined in terms of HAL_GetTick()
 * units. Typically it is 1ms value. The timeout determines when we would
 * consider the host "too slow" and threat the USB CDC port as disconnected.
 */
#ifndef USB_CDC_TRANSMIT_TIMEOUT
#define USB_CDC_TRANSMIT_TIMEOUT 3
#endif

/* USBD_CDC Private Variables */
/* USB Device Core CDC handle declaration */
USBD_HandleTypeDef hUSBD_Device_CDC;

static bool CDC_initialized = false;
static bool CDC_DTR_enabled = false;

/* Received Data over USB are stored in this buffer       */
CDC_TransmitQueue_TypeDef TransmitQueue;
CDC_ReceiveQueue_TypeDef ReceiveQueue;
__IO bool dtrState_Queue = true; /* lineState */
__IO bool rtsState_Queue = false;
__IO bool receivePended_Queue = true;
static uint32_t transmitStart = 0;

#ifdef DTR_TOGGLING_SEQ
/* DTR toggling sequence management */
extern void dtr_togglingHook(uint8_t *buf, uint32_t *len);
uint8_t dtr_toggling = 0;
#endif

/** USBD_CDC Private Function Prototypes */

static int8_t USBD_CDC_Init();

static int8_t USBD_CDC_DeInit();

static int8_t USBD_CDC_Control(uint8_t cmd, uint8_t *pbuf, uint16_t length);

static int8_t USBD_CDC_Receive(uint8_t *pbuf, uint32_t *Len);

static int8_t USBD_CDC_TransmitCplt(uint8_t *Bbuf, uint32_t *Len,
                                    uint8_t epnum);

void *rx_buffer;
USBD_CDC_ItfTypeDef USBD_CDC_fops_Queue = {USBD_CDC_Init, USBD_CDC_DeInit,
                                           USBD_CDC_Control, USBD_CDC_Receive,
                                           USBD_CDC_TransmitCplt};

/* Private functions ---------------------------------------------------------*/

/**
 * @brief  USBD_CDC_Init
 *         Initializes the CDC media low layer
 * @param  None
 * @retval Result of the operation: USBD_OK if all operations are OK else
 * USBD_FAIL
 */
static int8_t USBD_CDC_Init() {
  /* Set Application Buffers */
  CDC_TransmitQueue_Init(&TransmitQueue);
  CDC_ReceiveQueue_Init(&ReceiveQueue);
  receivePended_Queue = true;
  USBD_CDC_SetRxBuffer(&hUSBD_Device_CDC,
                       CDC_ReceiveQueue_ReserveBlock(&ReceiveQueue));

  return ((int8_t) USBD_OK);
}

/**
 * @brief  USBD_CDC_DeInit
 *         DeInitializes the CDC media low layer
 * @param  None
 * @retval Result of the operation: USBD_OK if all operations are OK else
 * USBD_FAIL
 */
static int8_t USBD_CDC_DeInit() { return ((int8_t) USBD_OK); }

/**
 * @brief  USBD_CDC_Control
 *         Manage the CDC class requests
 * @param  cmd: Command code
 * @param  pbuf: Buffer containing command data (request parameters)
 * @param  length: Number of data to be sent (in bytes)
 * @retval Result of the operation: USBD_OK if all operations are OK else
 * USBD_FAIL
 */
static int8_t USBD_CDC_Control(uint8_t cmd, uint8_t *pbuf, uint16_t length) {
  uint8_t tempbuf[7] = {0, 0, 0, 0, 0, 0, 0};
  UNUSED(length);
  switch (cmd) {
    case CDC_SEND_ENCAPSULATED_COMMAND:

      break;

    case CDC_GET_ENCAPSULATED_RESPONSE:

      break;

    case CDC_SET_COMM_FEATURE:

      break;

    case CDC_GET_COMM_FEATURE:

      break;

    case CDC_CLEAR_COMM_FEATURE:

      break;

          /*******************************************************************************/
          /* Line Coding Structure */
          /*-----------------------------------------------------------------------------*/
          /* Offset | Field       | Size | Value  | Description */
          /* 0      | dwDTERate   |   4  | Number |Data terminal rate, in bits per
           * second*/
          /* 4      | bCharFormat |   1  | Number | Stop bits */
          /*                                        0 - 1 Stop bit */
          /*                                        1 - 1.5 Stop bits */
          /*                                        2 - 2 Stop bits */
          /* 5      | bParityType |  1   | Number | Parity */
          /*                                        0 - None */
          /*                                        1 - Odd */
          /*                                        2 - Even */
          /*                                        3 - Mark */
          /*                                        4 - Space */
          /* 6      | bDataBits  |   1   | Number Data bits (5, 6, 7, 8 or 16). */
          /*******************************************************************************/
    case CDC_SET_LINE_CODING:
      tempbuf[0] = pbuf[0];
          tempbuf[1] = pbuf[1];
          tempbuf[2] = pbuf[2];
          tempbuf[3] = pbuf[3];
          tempbuf[4] = pbuf[4];
          tempbuf[5] = pbuf[5];
          tempbuf[6] = pbuf[6];
          break;

    case CDC_GET_LINE_CODING:
      pbuf[0] = tempbuf[0];
          pbuf[1] = tempbuf[1];
          pbuf[2] = tempbuf[2];
          pbuf[3] = tempbuf[3];
          pbuf[4] = tempbuf[4];
          pbuf[5] = tempbuf[5];
          pbuf[6] = tempbuf[6];
          break;

    case CDC_SET_CONTROL_LINE_STATE:
      // Check DTR state
//        dtrState_Queue = (CDC_DTR_enabled) ? (((USBD_SetupReqTypedef
//        *)pbuf)->wValue & CLS_DTR) : true;
//
//        if (dtrState_Queue)
//        { // Reset the transmit timeout when the port is connected
//            transmitStart = 0;
//        }
//        rtsState_Queue = (((USBD_SetupReqTypedef *)pbuf)->wValue & CLS_RTS);
#ifdef DTR_TOGGLING_SEQ
      dtr_toggling++; /* Count DTR toggling */
#endif
      break;

    case CDC_SEND_BREAK:

      break;

    default:
      break;
  }

  return ((int8_t) USBD_OK);
}

void CDC_init_Queue() {
  if (!CDC_initialized) {
    /* Init Device Library */
    if (USBD_Init(&hUSBD_Device_CDC, &FS_Desc, 0) == USBD_OK) {
      /* Add Supported Class */
      if (USBD_RegisterClass(&hUSBD_Device_CDC, USBD_CDC_CLASS) == USBD_OK) {
        /* Add CDC Interface Class */
        if (USBD_CDC_RegisterInterface(&hUSBD_Device_CDC,
                                       &USBD_CDC_fops_Queue) == USBD_OK) {
          /* Start Device Process */
          USBD_Start(&hUSBD_Device_CDC);
          CDC_initialized = true;
        }
      }
    }
  }
}

void CDC_deInit_Queue() {
  if (CDC_initialized) {
    HAL_PCD_DevDisconnect((PCD_HandleTypeDef *) hUSBD_Device_CDC.pData);
    USBD_Stop(&hUSBD_Device_CDC);
    USBD_CDC_DeInit();
    USBD_DeInit(&hUSBD_Device_CDC);
    USB_DevDisconnect(USBD_USB_INSTANCE);
    CDC_initialized = false;
  }
}

bool CDC_connected_Queue() {
  /* Save the transmitStart value in a local variable to avoid twice reading -
   * fix #478 */
  uint32_t transmitTime = transmitStart;
  if (transmitTime) {
    transmitTime = HAL_GetTick() - transmitTime;
  }
  return ((hUSBD_Device_CDC.dev_state == USBD_STATE_CONFIGURED) &&
          (transmitTime < USB_CDC_TRANSMIT_TIMEOUT) && dtrState_Queue);
}

void CDC_continue_transmit_Queue() {
  uint16_t size;
  uint8_t *buffer;
  auto *hcdc = (USBD_CDC_HandleTypeDef *) hUSBD_Device_CDC.pClassData;
  /*
   * TS: This method can be called both in the main thread
   * (via USBSerial::write) and in the IRQ stream (via USBD_CDC_TransmistCplt),
   * BUT the main thread cannot pass this condition while waiting for a IRQ!
   * This is not possible because TxState is not zero while waiting for data
   * transfer ending! The IRQ thread is uninterrupted, since its priority
   * is higher than that of the main thread. So this method is thread safe.
   */
  if (hcdc->TxState == 0U) {
    buffer = CDC_TransmitQueue_ReadBlock(&TransmitQueue, &size);
    if (size > 0) {
      transmitStart = HAL_GetTick();
      USBD_CDC_SetTxBuffer(&hUSBD_Device_CDC, buffer, size);
      /*
       * size never exceed PMA buffer and USBD_CDC_TransmitPacket make full
       * copy of block in PMA, so no need to worry about buffer damage
       */
      USBD_CDC_TransmitPacket(&hUSBD_Device_CDC);
    }
  }
}

/**
 * @brief  USBD_CDC_TransmitCplt
 *         Data transmitted callback
 *
 *         @note
 *         This function is IN transfer complete callback used to inform user
 * that the submitted Data is successfully sent over USB.
 *
 * @param  Bbuf: Buffer of data to be received
 * @param  Len: Number of data received (in bytes)
 * @retval Result of the operation: USBD_OK if all operations are OK else
 * USBD_FAIL
 */
static int8_t USBD_CDC_TransmitCplt(uint8_t *Bbuf, uint32_t *Len,
                                    uint8_t epnum) {
  UNUSED(Bbuf);
  UNUSED(Len);
  UNUSED(epnum);
  transmitStart = 0;
  CDC_TransmitQueue_CommitRead(&TransmitQueue);
  CDC_continue_transmit_Queue();
  return ((int8_t) USBD_OK);
}

bool CDC_resume_receive_Queue() {
  /*
   * TS: main and IRQ threads can't pass it at same time, because
   * IRQ may occur only if receivePended_Queue is true. So it is thread-safe!
   */
  if (!receivePended_Queue) {
    uint8_t *block = CDC_ReceiveQueue_ReserveBlock(&ReceiveQueue);
    if (block != nullptr) {
      receivePended_Queue = true;
      /* Set new buffer */
      USBD_CDC_SetRxBuffer(&hUSBD_Device_CDC, block);
      USBD_CDC_ReceivePacket(&hUSBD_Device_CDC);
      return true;
    }
  }
  return false;
}

#ifdef USE_USBD_COMPOSITE
uint8_t USBD_CDC_ClearBuffer(USBD_HandleTypeDef *pdev, uint8_t ClassId) {
  /* Suspend or Resume USB Out process */
  if (pdev->pClassDataCmsit[classId] != NULL) {
#else

uint8_t USBD_CDC_ClearBuffer(USBD_HandleTypeDef *pdev) {
  /* Suspend or Resume USB Out process */
  if (pdev->pClassDataCmsit[pdev->classId] != nullptr) {
#endif /* USE_USBD_COMPOSITE */
    /* Prepare Out endpoint to receive next packet */
    USBD_LL_PrepareReceive(pdev, CDC_OUT_EP, nullptr, 0);
    return (uint8_t) USBD_OK;
  } else {
    return (uint8_t) USBD_FAIL;
  }
}

/**
 * @brief  USBD_CDC_Receive
 *         Data received over USB OUT endpoint are sent over CDC interface
 *         through this function.
 *
 *         @note
 *         This function will issue a NAK packet on any OUT packet received on
 *         USB endpoint until exiting this function. If you exit this function
 *         before transfer is complete on CDC interface (ie. using DMA
 * controller) it will result in receiving more data while previous ones are
 * still not sent.
 *
 * @param  pbuf: Buffer of data to be received
 * @param  Len: Number of data received (in bytes)
 * @retval Result of the operation: USBD_OK if all operations are OK else
 * USBD_FAIL
 */
static int8_t USBD_CDC_Receive(uint8_t *pbuf, uint32_t *Len) {
#ifdef DTR_TOGGLING_SEQ
  if (dtr_toggling > 3) {
    dtr_togglingHook(Buf, Len);
    dtr_toggling = 0;
  }
#else
  UNUSED(pbuf);
#endif
  /* It always contains required amount of free space for writing */
  CDC_ReceiveQueue_CommitBlock(&ReceiveQueue, (uint16_t) (*Len));
  receivePended_Queue = false;
  /* If enough space in the queue for a full buffer then continue receive */
  if (!CDC_resume_receive_Queue()) {
    USBD_CDC_ClearBuffer(&hUSBD_Device_CDC);
  }
#ifdef USE_FREERTOS
  if (*Len > 0) {
    Rtos::SemaphoreReleaseISR(rx_buffer);
  }
#endif
  return ((int8_t) USBD_OK);
}

void CDC_enableDTR_Queue(bool enable) { CDC_DTR_enabled = enable; }

int USB_Serial::available() {
  // Just ReceiveQueue size, available for reading
  return CDC_ReceiveQueue_ReadSize(&ReceiveQueue);
}

int USB_Serial::read() {
  // Dequeue only one char from queue
  // TS: it safe, because only main thread affects ReceiveQueue->read pos
  auto ch = CDC_ReceiveQueue_Dequeue(&ReceiveQueue);
  // Resume receive process, if possible
  CDC_resume_receive_Queue();
  return ch;
}

/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
#define USB_DM_Pin GPIO_PIN_11

#define USB_DP_Pin GPIO_PIN_12

void DisconnectUsbPort() {

  pinMode(PA11, OUTPUT);
  pinMode(PA12, OUTPUT);
  digitalWrite(PA11, LOW);
  digitalWrite(PA12, LOW);
}

void USB_Serial::begin(uint32_t /* baud_count */) {
  // uart config is ignored in USB-CDC
  begin();
}

void USB_Serial::begin(uint32_t baud, uint8_t config) {
  // uart config is ignored in USB-CDC
  begin();
}

#ifndef THIS_IS_BOOTLOADER

int USB_Serial::end() {
  // DisconnectUsbPort();
  CDC_deInit_Queue();
  return 0;
}

#endif

int USB_Serial::availableForWrite() {
  // Just transmit queue size, available for write
  return CDC_TransmitQueue_WriteSize(&TransmitQueue);
}

int USB_Serial::write(uint8_t ch) {
  // Just write single-byte buffer.
  return write(&ch, 1);
}

int USB_Serial::write(const uint8_t *buffer, int size) {
  int rest = size;
  while ((rest > 0) && CDC_connected_Queue()) {
    // Determine buffer size available for write
    auto portion = CDC_TransmitQueue_WriteSize(&TransmitQueue);
    // Truncate it to content size (if rest is greater)
    if (rest < portion) {
      portion = rest;
    }
    if (portion > 0) {
      // Only if some space in the buffer exists.
      // TS: Only main thread calls write and writeSize methods,
      // it's thread-safe since IRQ does not affects
      // TransmitQueue write position
      CDC_TransmitQueue_Enqueue(&TransmitQueue, buffer, portion);
      rest -= portion;
      buffer += portion;
      // After storing data, start transmitting process
      CDC_continue_transmit_Queue();
    }
  }
  return size - rest;
}

void USB_Serial::flush() {
  // Wait for TransmitQueue read size becomes zero
  // TS: safe, because it not be stopped while receive 0
  // while (CDC_TransmitQueue_ReadSize(&TransmitQueue) > 0)
  // {
  // }
}

void USB_Serial::dtr(bool enable) { CDC_enableDTR_Queue(enable); }

bool USB_Serial::dtr() { return dtrState_Queue; }

bool USB_Serial::rts() { return rtsState_Queue; }

int USB_Serial::read_wait(int timeout) {
#ifdef USE_FREERTOS
  if (!available()) {
    Rtos::SemaphoreWait(rx_buffer);
  }
#endif
  return read();
}

bool usb_disconnect = false;
extern PCD_HandleTypeDef g_hpcd;

bool USB_Serial::disconnected() {
#ifdef __PLATFORMIO_BUILD_DEBUG__
  // if (READ_BIT(RCC->CRRCR, RCC_HSI48_ON) == 0)
  // {
  //     logger.error("HSI48 is off");
  // }
#endif
  // USB_OTG_GlobalTypeDef *USBx = g_hpcd.Instance;
  // uint32_t USBx_BASE = (uint32_t)USBx;
  return USBD_STATE_SUSPENDED == hUSBD_Device_CDC.dev_state;
}

extern "C" {
void HAL_PCD_SuspendCallback1(PCD_HandleTypeDef *hpcd) {
  USB_OTG_GlobalTypeDef *USBx = hpcd->Instance;
  auto USBx_BASE = (uint32_t) USBx;
  core_debug("Suspend");
  //        if (hpcd->Init.use_dedicated_ep1 != 0U)
  //        {
  //            USBx_DEVICE->DOUTEP1MSK = 0;
  //
  //            USBx_DEVICE->DINEP1MSK = 0;
  //        }
  //        else
  //        {
  //            USBx_DEVICE->DOEPMSK = 0;
  //
  //            USBx_DEVICE->DIEPMSK = 0;
  //        }
  //        CLEAR_BIT(USBx->GINTMSK, USB_OTG_GINTMSK_RXFLVLM);
  __HAL_PCD_GATE_PHYCLOCK(hpcd);
  USBD_LL_Suspend((USBD_HandleTypeDef *) hpcd->pData);
  //        // if (USBD_LL_DevConnected((USBD_HandleTypeDef *)hpcd->pData) ==
  //        USBD_OK)
  //        // {
  //        //     return;
  //        // }
  //
  //        /*Enter in STOP mode */
  //        if (hpcd->Init.low_power_enable)
  //        {
  //            /* Set SLEEPDEEP bit and SleepOnExit of Cortex System Control
  //            Register */
  //            // SCB->SCR |= (uint32_t)((uint32_t)(SCB_SCR_SLEEPDEEP_Msk |
  //            SCB_SCR_SLEEPONEXIT_Msk));
  //        }
  //        vUlpOnPeripheralsInactiveFromISR(ulpPERIPHERAL_LPTIM2);
  //        usb_disconnect = true;
  // CLEAR_BIT(RCC->CRRCR, RCC_HSI48_ON);
  // CLEAR_BIT(hpcd->Instance->GINTSTS, USB_OTG_GINTSTS_ESUSP);
}
void HAL_PCD_ResumeCallback1(PCD_HandleTypeDef *hpcd) {
  //        USB_OTG_GlobalTypeDef *USBx = hpcd->Instance;
  //        uint32_t USBx_BASE = (uint32_t)USBx;
  //        HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9);
  //        HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9);
  //        HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9);
  //        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9) == GPIO_PIN_RESET)
  //        {
  //             core_debug("Resume cancel");
  //            return;
  //        }
  //        SET_BIT(RCC->CRRCR, RCC_HSI48_ON);
  //        usb_disconnect = false;
  __HAL_PCD_UNGATE_PHYCLOCK(hpcd);
  //        if (hpcd->Init.low_power_enable)
  //        {
  //            // USBD_SystemClockConfigFromResume();
  //
  //            /* Reset SLEEPDEEP bit of Cortex System Control Register */
  //            // SCB->SCR &= (uint32_t) ~((uint32_t)(SCB_SCR_SLEEPDEEP_Msk |
  //            SCB_SCR_SLEEPONEXIT_Msk));
  //        }
  USBD_LL_Resume((USBD_HandleTypeDef *) hpcd->pData);
  //        USBx->GINTMSK |= USB_OTG_GINTMSK_RXFLVLM;
  //        vUlpOnPeripheralsActiveFromISR(ulpPERIPHERAL_LPTIM2);
  core_debug("Resume");
}
//    void HAL_PCD_DisconnectCallback(PCD_HandleTypeDef *hpcd)
//    {
//        logger.debug("USB_Serial::HAL_PCD_DisconnectCallback");
//        USBD_LL_DevDisconnected((USBD_HandleTypeDef *)hpcd->pData);
//    }
//    void HAL_PCD_ConnectCallback(PCD_HandleTypeDef *hpcd)
//    {
//        logger.debug("USB_Serial::HAL_PCD_ConnectCallback");
//        USBD_LL_DevConnected((USBD_HandleTypeDef *)hpcd->pData);
//    }
}
#endif // USBCON && USBD_USE_CDC
